<!--
  文件用途：系统登录页（仅注释，不改逻辑）
  视觉效果：雪花画布 + 山景 SVG，登录卡片顶部可堆积雪效果
  交互流程：
  - 输入用户名/密码，点击登录
  - 成功后写入 localStorage userId/username，sessionStorage role
  - 按 preferredPanel 决定跳转 Admin 或 User 面板
-->
<template>
  <div class="login-wrap">
    <!-- 雪花画布 -->
    <canvas ref="snowCanvas" class="snow-canvas"></canvas>
    <!-- 山景 SVG 背景 -->
    <svg class="mountains" viewBox="0 0 1000 260" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
      <defs>
        <linearGradient id="mountainFade" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#8faed4" stop-opacity="0.9" />
          <stop offset="100%" stop-color="#6c86a9" stop-opacity="0.55" />
        </linearGradient>
        <linearGradient id="mountainFade2" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stop-color="#7c9cc4" stop-opacity="0.85" />
          <stop offset="100%" stop-color="#58769a" stop-opacity="0.55" />
        </linearGradient>
      </defs>
      <path d="M0 210 L120 150 L200 178 L290 132 L380 180 L470 118 L560 168 L640 136 L740 170 L820 150 L900 188 L1000 140 L1000 260 L0 260 Z" fill="url(#mountainFade)" />
      <path d="M0 230 L140 170 L210 196 L300 150 L390 198 L480 142 L560 190 L650 156 L740 198 L830 168 L910 210 L1000 170 L1000 260 L0 260 Z" fill="url(#mountainFade2)" />
    </svg>
  <div class="login-panel" ref="loginPanel">
      <div class="brand-side">
        <div class="brand-inner">
          <h1 class="site-title">TodayJob</h1>
          <p class="site-sub">一体化工作流 · 任务 · 看板 · 日志</p>
          <ul class="highlights">
            <li>统一工作台</li>
            <li>角色分离</li>
            <li>实时协作</li>
          </ul>
        </div>
      </div>
      <el-card class="box" shadow="never">
        <div class="card-header">
          <h2>系统登录</h2>
          <p class="subtitle">欢迎使用 TodayJob 管理与协同平台</p>
        </div>
        <el-form :model="form" @submit.prevent class="login-form" label-position="top">
          <el-form-item label="用户名">
            <el-input v-model="form.username" placeholder="请输入用户名" clearable>
              <template #prefix><el-icon><User /></el-icon></template>
            </el-input>
          </el-form-item>
          <el-form-item label="密码">
            <el-input v-model="form.password" placeholder="请输入密码" show-password clearable>
              <template #prefix><el-icon><Lock /></el-icon></template>
            </el-input>
          </el-form-item>
          <el-form-item>
            <el-button ref="loginBtn" type="primary" :loading="loading" @click="login" class="login-btn" round block>登录</el-button>
          </el-form-item>
          <el-form-item>
            <el-button type="warning" plain size="small" :disabled="stick.active" @click="clearSnow">清雪</el-button>
          </el-form-item>
        </el-form>
        <div class="assist-row">
          <div class="tips"><el-icon><InfoFilled /></el-icon> 管理员进入管理后台；普通用户进入用户工作台</div>
        </div>
      </el-card>
    </div>
  </div>
</template>

<script setup>
import { reactive, ref, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { api } from '@/utils/api'
import { ElMessage } from 'element-plus'
import { User, Lock, InfoFilled } from '@element-plus/icons-vue'

const router = useRouter()
const form = reactive({ username: '', password: '' })
const loading = ref(false)
const snowCanvas = ref(null)
const loginPanel = ref(null)
const loginBtn = ref(null)
let snowCtx = null
let flakes = []
let animationId = 0
let chunks = []        // 顶部掉落雪团
let bursts = []        // 冲击波爆裂雪粒
let frameCount = 0
let rings = []         // 冲击波扩散环
let stick = { active: false, phase: 'idle', x: 0, height: 0, sweepX: 0, sweepSpeed: 9 }

const CONFIG = {
  density: 0.12, // 提高密度，加快积累
  maxSize: 4.5,
  minSize: 1.2,
  wind: 0.35, // 水平微风系数
  accumulate: true,
  accColumns: 160, // 底部堆积高度列数（越大越平滑，开销稍增）
  accMaxDepth: 110, // 地面最大堆积厚度（像素）
  panelAccumulate: true,
  panelAccColumns: 120,
  panelAccMaxDepth: 42 // 卡片顶部最大堆积高度
}

let accHeights = [] // 每列积雪高度
let accColWidth = 0
let accMaxReached = false

// 卡片顶部积雪
let panelAccHeights = []
let panelColWidth = 0
let panelAccMaxReached = false
let panelRectCache = null

function createFlake(width, height) {
  const size = Math.random() * (CONFIG.maxSize - CONFIG.minSize) + CONFIG.minSize
  return {
    x: Math.random() * width,
    y: -Math.random() * height, // 初始随机分布在上方以实现连续感
    r: size,
    speed: size * 0.35 + Math.random() * 0.4 + 0.4,
    drift: (Math.random() - 0.5) * CONFIG.wind,
    opacity: 0.55 + Math.random() * 0.45
  }
}

function initFlakes(count, width, height) {
  flakes = Array.from({ length: count }, () => createFlake(width, height))
}

function resizeCanvas() {
  if (!snowCanvas.value) return
  const dpr = window.devicePixelRatio || 1
  const rect = snowCanvas.value.getBoundingClientRect()
  snowCanvas.value.width = rect.width * dpr
  snowCanvas.value.height = rect.height * dpr
  snowCtx = snowCanvas.value.getContext('2d')
  snowCtx.scale(dpr, dpr)
  const baseCount = Math.min(180, Math.floor(rect.width * CONFIG.density))
  initFlakes(baseCount, rect.width, rect.height)
  // 初始化/重置积雪高度
  accColWidth = rect.width / CONFIG.accColumns
  accHeights = new Array(CONFIG.accColumns).fill(0)
  accMaxReached = false
  panelAccHeights = []
  panelAccMaxReached = false
  panelRectCache = null
  chunks = []
  bursts = []
  frameCount = 0
  rings = []
  stick = { active: false, phase: 'idle', x: 0, height: 0, sweepX: 0, sweepSpeed: 9 }
}

function animateSnow() {
  if (!snowCtx) return
  const { width, height } = snowCanvas.value.getBoundingClientRect()
  snowCtx.clearRect(0, 0, width, height)
  // 更新卡片矩形缓存
  if (loginPanel.value) {
    const r = loginPanel.value.getBoundingClientRect()
    if (!panelRectCache || panelRectCache.width !== r.width || panelRectCache.left !== r.left || panelRectCache.top !== r.top) {
      panelRectCache = { left: r.left, top: r.top, width: r.width, height: r.height }
      if (CONFIG.panelAccumulate) {
        panelColWidth = r.width / CONFIG.panelAccColumns
        panelAccHeights = new Array(CONFIG.panelAccColumns).fill(0)
        panelAccMaxReached = false
      }
    }
  }
  // 先绘制底部地面积雪
  if (CONFIG.accumulate) drawGroundAccumulation(width, height)

  frameCount++
  // 顶部积雪满后周期性生成雪团往下掉
  // 条件放宽：顶部积雪高度超过 60% 开始掉落雪团；完全满时频率更高
  const panelPeak = panelAccHeights.length ? Math.max(...panelAccHeights) : 0
  if (panelPeak > CONFIG.panelAccMaxDepth * 0.6 && panelRectCache) {
    const interval = panelAccMaxReached ? 18 : 40
    if (frameCount % interval === 0 && chunks.length < 50) spawnChunk(panelAccMaxReached ? 2 : 1)
  }

  snowCtx.fillStyle = '#ffffff'
  flakes.forEach(f => {
    f.y += f.speed
    f.x += f.drift + Math.sin((f.y + f.r) * 0.01) * 0.3 // 轻微摆动

    // 计算雪花当前所处的“地面”：可能是地面或卡片顶部
    let groundY = height
    let targetArray = 'ground'
    let groundIndex = -1

    // 地面高度
    if (CONFIG.accumulate) {
      const idx = Math.min(CONFIG.accColumns - 1, Math.max(0, Math.floor(f.x / accColWidth)))
      const baseH = accHeights[idx] || 0
      groundY = height - baseH
      groundIndex = idx
    }

    // 若在卡片范围内，尝试用卡片顶部“地面”替换
    if (CONFIG.panelAccumulate && panelRectCache) {
      const canvasRect = snowCanvas.value.getBoundingClientRect()
      const panelLeft = panelRectCache.left - canvasRect.left
      const panelTop = panelRectCache.top - canvasRect.top
      const panelRight = panelLeft + panelRectCache.width
      if (f.x >= panelLeft && f.x <= panelRight) {
        const pIdx = Math.min(CONFIG.panelAccColumns - 1, Math.max(0, Math.floor((f.x - panelLeft) / panelColWidth)))
        const panelH = panelAccHeights[pIdx] || 0
        const topGround = panelTop - panelH
        if (topGround < groundY) { // 卡片顶部优先生效（更高的位置）
          groundY = topGround
          targetArray = 'panel'
          groundIndex = pIdx
        }
      }
    }

    // 撞击
    if (f.y + f.r >= groundY) {
      if (targetArray === 'ground' && CONFIG.accumulate && !accMaxReached) {
        accumulateToGround(groundIndex, f.r)
      } else if (targetArray === 'panel' && CONFIG.panelAccumulate && !panelAccMaxReached) {
        accumulateToPanel(groundIndex, f.r)
      }
      f.y = -f.r - Math.random() * 40
      f.x = Math.random() * width
      return
    }

    if (f.x < -10) f.x = width + 5
    if (f.x > width + 10) f.x = -5
    snowCtx.globalAlpha = f.opacity
    snowCtx.beginPath()
    snowCtx.arc(f.x, f.y, f.r, 0, Math.PI * 2)
    snowCtx.closePath()
    snowCtx.fill()
  })

  // 雪团更新与绘制
  updateChunks(width, height)
  drawChunks()

  // 冲击波爆裂雪粒
  updateBursts(width, height)
  drawBursts()

  // 冲击环
  updateRings()
  drawRings()

  // 绘制卡片顶部积雪（放在最后保证覆盖）
  if (CONFIG.panelAccumulate && panelRectCache) drawPanelAccumulation(width, height)
  // 绘制清雪棍
  if (stick.active) drawStick()
  animationId = requestAnimationFrame(animateSnow)
}

function smoothAccumulation(arr) {
  for (let i = 1; i < arr.length - 1; i++) {
    arr[i] = (arr[i - 1] * 0.15 + arr[i] * 0.7 + arr[i + 1] * 0.15)
  }
}

function accumulateToGround(idx, r) {
  if (idx < 0) return
  accHeights[idx] += Math.max(1.2, r * 1.5) // 提升主列增量
  if (accHeights[idx] > CONFIG.accMaxDepth) accHeights[idx] = CONFIG.accMaxDepth
  if (idx > 0) accHeights[idx - 1] += r * 0.25
  if (idx < CONFIG.accColumns - 1) accHeights[idx + 1] += r * 0.25
  if (idx > 0 && accHeights[idx - 1] > CONFIG.accMaxDepth) accHeights[idx - 1] = CONFIG.accMaxDepth
  if (idx < CONFIG.accColumns - 1 && accHeights[idx + 1] > CONFIG.accMaxDepth) accHeights[idx + 1] = CONFIG.accMaxDepth
  smoothAccumulation(accHeights)
  postProcessGround()
  if (Math.max(...accHeights) >= CONFIG.accMaxDepth) accMaxReached = true
}

function accumulateToPanel(idx, r) {
  if (idx < 0) return
  panelAccHeights[idx] += Math.max(0.9, r * 1.3)
  if (panelAccHeights[idx] > CONFIG.panelAccMaxDepth) panelAccHeights[idx] = CONFIG.panelAccMaxDepth
  if (idx > 0) panelAccHeights[idx - 1] += r * 0.2
  if (idx < CONFIG.panelAccColumns - 1) panelAccHeights[idx + 1] += r * 0.2
  if (idx > 0 && panelAccHeights[idx - 1] > CONFIG.panelAccMaxDepth) panelAccHeights[idx - 1] = CONFIG.panelAccMaxDepth
  if (idx < CONFIG.panelAccColumns - 1 && panelAccHeights[idx + 1] > CONFIG.panelAccMaxDepth) panelAccHeights[idx + 1] = CONFIG.panelAccMaxDepth
  smoothAccumulation(panelAccHeights)
  postProcessPanel()
  const peak = Math.max(...panelAccHeights)
  if (peak >= CONFIG.panelAccMaxDepth) {
    panelAccMaxReached = true
  } else if (panelAccMaxReached && peak < CONFIG.panelAccMaxDepth * 0.65) {
    // 若高度显著下降（清雪后）则允许再次堆满
    panelAccMaxReached = false
  }
}

function drawGroundAccumulation(width, height) {
  if (!accHeights.length) return
  snowCtx.save()
  const grad = snowCtx.createLinearGradient(0, height - CONFIG.accMaxDepth, 0, height)
  grad.addColorStop(0, 'rgba(255,255,255,0.92)')
  grad.addColorStop(1, 'rgba(230,234,244,0.85)')
  snowCtx.fillStyle = grad
  snowCtx.beginPath()
  snowCtx.moveTo(0, height)
  for (let i = 0; i < CONFIG.accColumns; i++) {
    const x = i * accColWidth
    const y = height - accHeights[i]
    snowCtx.lineTo(x, y)
  }
  snowCtx.lineTo(width, height)
  snowCtx.closePath()
  snowCtx.fill()
  // 高光
  snowCtx.globalAlpha = 0.35
  snowCtx.beginPath()
  snowCtx.moveTo(0, height)
  for (let i = 0; i < CONFIG.accColumns; i++) {
    const x = i * accColWidth
    const y = height - accHeights[i] - 2
    snowCtx.lineTo(x, y)
  }
  snowCtx.lineTo(width, height)
  snowCtx.closePath()
  snowCtx.fillStyle = 'rgba(255,255,255,0.55)'
  snowCtx.fill()
  snowCtx.restore()
}

function drawPanelAccumulation() {
  if (!panelRectCache || !panelAccHeights.length) return
  snowCtx.save()
  const canvasRect = snowCanvas.value.getBoundingClientRect()
  const px = panelRectCache.left - canvasRect.left
  const py = panelRectCache.top - canvasRect.top
  const pw = panelRectCache.width
  // 清除卡片内部（不影响顶部向上堆积）
  snowCtx.clearRect(px, py, pw, panelRectCache.height + 4)
  const grad = snowCtx.createLinearGradient(0, py - CONFIG.panelAccMaxDepth, 0, py)
  grad.addColorStop(0, 'rgba(255,255,255,0)')
  grad.addColorStop(0.45, 'rgba(255,255,255,0.55)')
  grad.addColorStop(1, 'rgba(255,255,255,0.96)')
  snowCtx.fillStyle = grad

  const CORNER_RADIUS = 20
  const inset = CORNER_RADIUS * 0.6
  const TAPER = 70
  snowCtx.beginPath()
  snowCtx.moveTo(px + inset, py)
  for (let i = 0; i < CONFIG.panelAccColumns; i++) {
    const localX = i * panelColWidth
    if (localX < inset || localX > pw - inset) continue
    const x = px + localX
    const distEdge = Math.min(localX - inset, (pw - inset) - localX)
    const norm = Math.min(1, distEdge / TAPER)
    const factor = norm * norm * (3 - 2 * norm)
    const h = panelAccHeights[i] * factor
    const y = py - h
    snowCtx.lineTo(x, y)
  }
  snowCtx.lineTo(px + pw - inset, py)
  snowCtx.closePath()
  snowCtx.fill()

  snowCtx.globalAlpha = 0.5
  snowCtx.beginPath()
  snowCtx.moveTo(px + inset, py)
  for (let i = 0; i < CONFIG.panelAccColumns; i++) {
    const localX = i * panelColWidth
    if (localX < inset || localX > pw - inset) continue
    const x = px + localX
    const distEdge = Math.min(localX - inset, (pw - inset) - localX)
    const norm = Math.min(1, distEdge / TAPER)
    const factor = norm * norm * (3 - 2 * norm)
    const h = panelAccHeights[i] * factor
    const y = py - h - 1.3
    snowCtx.lineTo(x, y)
  }
  snowCtx.lineTo(px + pw - inset, py)
  snowCtx.closePath()
  snowCtx.fillStyle = 'rgba(255,255,255,0.65)'
  snowCtx.fill()
  snowCtx.restore()
}

// --- 后处理：让地面更水平、边缘不突起 ---
function postProcessGround() {
  if (!accHeights.length) return
  limitSlope(accHeights, 5)
  // 边缘压低，避免两侧高起
  accHeights[0] = accHeights[1] * 0.6
  accHeights[accHeights.length - 1] = accHeights[accHeights.length - 2] * 0.6
  // 向整体平均值收敛，拉平局部峰值
  const avg = accHeights.reduce((a, b) => a + b, 0) / accHeights.length
  for (let i = 0; i < accHeights.length; i++) {
  accHeights[i] = accHeights[i] * 0.94 + avg * 0.06 // 减弱回拉，保持增长
  }
}

function postProcessPanel() {
  if (!panelAccHeights.length) return
  limitSlope(panelAccHeights, 4)
  const avg = panelAccHeights.reduce((a, b) => a + b, 0) / panelAccHeights.length
  for (let i = 0; i < panelAccHeights.length; i++) {
  panelAccHeights[i] = panelAccHeights[i] * 0.95 + avg * 0.05
  }
}

function limitSlope(arr, maxDiff) {
  for (let i = 0; i < arr.length - 1; i++) {
    const d = arr[i + 1] - arr[i]
    if (Math.abs(d) > maxDiff) {
      const adjust = (Math.abs(d) - maxDiff) * 0.5
      if (d > 0) {
        arr[i + 1] -= adjust
        arr[i] += adjust
      } else {
        arr[i + 1] += adjust
        arr[i] -= adjust
      }
    }
  }
}

// --- 雪团与冲击波 ---
function spawnChunk(count = 1) {
  const canvasRect = snowCanvas.value.getBoundingClientRect()
  const px = panelRectCache.left - canvasRect.left
  const pw = panelRectCache.width
  for (let n = 0; n < count; n++) {
    const x = px + pw * 0.08 + Math.random() * pw * 0.84
    const y = panelRectCache.top - canvasRect.top - (4 + Math.random() * 8)
    const r = 5 + Math.random() * 8
    chunks.push({ x, y, r, vx: (Math.random() - 0.5) * 0.7, vy: 0.3 + Math.random() * 0.5 })
  }
  if (chunks.length > 70) chunks.splice(0, chunks.length - 70)
}

function updateChunks(width, height) {
  if (!chunks.length) return
  for (let i = chunks.length - 1; i >= 0; i--) {
    const c = chunks[i]
    c.vy += 0.028
    c.x += c.vx
    c.y += c.vy
    if (c.y + c.r >= height) {
      // 砸到地面产生小爆裂
      const splash = 6 + Math.floor(Math.random() * 4)
      for (let k = 0; k < splash; k++) {
        const a = Math.random() * Math.PI
        const s = 1.2 + Math.random() * 2.8
        bursts.push({ x: c.x, y: height - 2, vx: Math.cos(a) * s, vy: -Math.abs(Math.sin(a) * s) - 0.6, r: 1 + Math.random() * 2.4, life: 50 + Math.random() * 25 })
      }
      const idx = Math.min(CONFIG.accColumns - 1, Math.max(0, Math.floor(c.x / accColWidth)))
      accHeights[idx] += c.r * 1.1
      chunks.splice(i, 1)
    }
  }
}

function drawChunks() {
  if (!chunks.length) return
  snowCtx.save()
  snowCtx.fillStyle = '#ffffff'
  chunks.forEach(c => {
    snowCtx.globalAlpha = 0.95
    snowCtx.beginPath()
    snowCtx.arc(c.x, c.y, c.r, 0, Math.PI * 2)
    snowCtx.fill()
  })
  snowCtx.restore()
}

function updateBursts(width, height) {
  if (!bursts.length) return
  for (let i = bursts.length - 1; i >= 0; i--) {
    const b = bursts[i]
  b.vy += 0.045
    b.x += b.vx
    b.y += b.vy
    b.life--
    if (b.life <= 0 || b.y > height + 30) bursts.splice(i,1)
  }
}

function drawBursts() {
  if (!bursts.length) return
  snowCtx.save()
  snowCtx.fillStyle = '#ffffff'
  bursts.forEach(b => {
    snowCtx.globalAlpha = Math.max(0, b.life / 80)
    snowCtx.beginPath()
    snowCtx.arc(b.x, b.y, b.r, 0, Math.PI * 2)
    snowCtx.fill()
  })
  snowCtx.restore()
}

function triggerShockwave() {
  if (!snowCanvas.value) return
  const canvasRect = snowCanvas.value.getBoundingClientRect()
  const btnRect = loginBtn.value?.$el ? loginBtn.value.$el.getBoundingClientRect() : loginBtn.value?.getBoundingClientRect()
  const cx = btnRect ? btnRect.left + btnRect.width / 2 - canvasRect.left : canvasRect.width / 2
  const cy = btnRect ? btnRect.top + btnRect.height / 2 - canvasRect.top : canvasRect.height / 2
  // 冲击环
  rings.push({ cx, cy, r: 0, max: Math.max(canvasRect.width, canvasRect.height) * 0.9, alpha: 0.6 })
  // 更激烈：遍历所有列
  for (let i = 0; i < accHeights.length; i++) {
    const h = accHeights[i]
    if (h < 1) continue
    const samples = Math.min(6, Math.max(2, Math.floor(h / 10) + 2))
    for (let s = 0; s < samples; s++) {
      const x = i * accColWidth + Math.random() * accColWidth
      const base = canvasRect.height - h
      const y = base + Math.random() * h * 0.3
      const angle = Math.atan2(y - cy, x - cx) + (Math.random() - 0.5) * 0.6
      const speed = 4 + Math.random() * 6
      bursts.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - (3 + Math.random() * 2), r: 1 + Math.random() * 2.5, life: 80 + Math.random() * 50 })
    }
    accHeights[i] *= 0.3
  }
  if (panelRectCache) {
    const canvasRect2 = snowCanvas.value.getBoundingClientRect()
    const px = panelRectCache.left - canvasRect2.left
    const py = panelRectCache.top - canvasRect2.top
    for (let i = 0; i < panelAccHeights.length; i++) {
      const h = panelAccHeights[i]
      if (h < 0.5) continue
      const samples = Math.min(5, Math.max(2, Math.floor(h / 6) + 1))
      for (let s = 0; s < samples; s++) {
        const x = px + i * panelColWidth + Math.random() * panelColWidth
        const y = py - h + Math.random() * h * 0.4
        const angle = Math.atan2(y - cy, x - cx) + (Math.random() - 0.5) * 0.8
        const speed = 4 + Math.random() * 5
        bursts.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - (3 + Math.random() * 1.5), r: 0.8 + Math.random() * 2, life: 70 + Math.random() * 40 })
      }
      panelAccHeights[i] *= 0.25
    }
    // 冲击波后可能需要解除“已满”标记
    if (panelAccMaxReached && Math.max(...panelAccHeights) < CONFIG.panelAccMaxDepth * 0.65) {
      panelAccMaxReached = false
    }
  }
}

// ---------- 清雪棍逻辑 ----------
function clearSnow() {
  if (!panelRectCache || stick.active) return
  const canvasRect = snowCanvas.value.getBoundingClientRect()
  const px = panelRectCache.left - canvasRect.left
  stick = {
    active: true,
    phase: 'raise',
    x: px + panelRectCache.width / 2,
    height: 0,
    sweepX: px + 14, // 起始稍微偏内，避免圆角
    sweepSpeed: 11
  }
  // 立即允许后续继续堆积
  panelAccMaxReached = false
}

function updateStick() {
  if (!stick.active || !panelRectCache) return
  const canvasRect = snowCanvas.value.getBoundingClientRect()
  const px = panelRectCache.left - canvasRect.left
  const py = panelRectCache.top - canvasRect.top
  const pw = panelRectCache.width
  const targetHeight = CONFIG.panelAccMaxDepth + 60

  if (stick.phase === 'raise') {
    stick.height += 8
    if (stick.height >= targetHeight) {
      stick.height = targetHeight
      stick.phase = 'sweep'
    }
  } else if (stick.phase === 'sweep') {
    stick.sweepX += stick.sweepSpeed
    // 推动附近积雪掉落
    const localX = stick.sweepX - px
    const range = 24
    if (localX >= 0 && localX <= pw) {
      const startIdx = Math.max(0, Math.floor((localX - range) / panelColWidth))
      const endIdx = Math.min(panelAccHeights.length - 1, Math.floor((localX + range) / panelColWidth))
      for (let i = startIdx; i <= endIdx; i++) {
        const h = panelAccHeights[i]
        if (h <= 0.5) continue
        // 生成下落碎雪
        const colX = px + i * panelColWidth + Math.random() * panelColWidth
        for (let k = 0; k < 2; k++) {
          bursts.push({ x: colX, y: py - h + Math.random() * 4, vx: (Math.random() - 0.5) * 2, vy: 0.5 + Math.random() * 1.5, r: 1 + Math.random() * 1.6, life: 45 + Math.random() * 25 })
        }
        // 转为地面落雪块
        chunks.push({ x: colX, y: py - 2, r: 3 + Math.random() * 3, vx: (Math.random() - 0.5) * 0.8, vy: 0.8 })
        panelAccHeights[i] *= 0.25
      }
    }
    if (stick.sweepX > px + pw - 14) {
      stick.phase = 'done'
    }
  } else if (stick.phase === 'done') {
    stick.height -= 10
    if (stick.height <= 0) {
      stick.active = false
      stick.phase = 'idle'
      // 扫雪完成后如果顶部高度已低于阈值，解除已满状态
      if (panelAccMaxReached && Math.max(...panelAccHeights) < CONFIG.panelAccMaxDepth * 0.65) {
        panelAccMaxReached = false
      }
    }
  }
}

function drawStick() {
  updateStick()
  if (!stick.active || !panelRectCache) return
  const canvasRect = snowCanvas.value.getBoundingClientRect()
  const py = panelRectCache.top - canvasRect.top
  snowCtx.save()
  snowCtx.globalAlpha = 0.85
  snowCtx.fillStyle = '#f2f5fa'
  const w = 10
  const x = stick.phase === 'raise' ? stick.x - w / 2 : stick.sweepX - w / 2
  const y = py - stick.height
  const radius = 4
  // 画带圆角的竖直棒
  snowCtx.beginPath()
  snowCtx.moveTo(x, y + radius)
  snowCtx.arcTo(x, y, x + w, y, radius)
  snowCtx.arcTo(x + w, y, x + w, y + stick.height, radius)
  snowCtx.arcTo(x + w, y + stick.height, x, y + stick.height, radius)
  snowCtx.arcTo(x, y + stick.height, x, y, radius)
  snowCtx.closePath()
  snowCtx.fill()
  // 顶部高光
  snowCtx.globalAlpha = 0.35
  snowCtx.fillStyle = '#ffffff'
  snowCtx.fillRect(x + 2, y + 2, w - 4, 6)
  snowCtx.restore()
}



function updateRings() {
  if (!rings.length) return
  for (let i = rings.length - 1; i >= 0; i--) {
    const ring = rings[i]
    ring.r += 18
    ring.alpha *= 0.92
    if (ring.alpha < 0.04) rings.splice(i,1)
  }
}

function drawRings() {
  if (!rings.length || !snowCtx) return
  snowCtx.save()
  rings.forEach(rg => {
    snowCtx.beginPath()
    snowCtx.strokeStyle = `rgba(255,255,255,${rg.alpha})`
    snowCtx.lineWidth = 3
    snowCtx.arc(rg.cx, rg.cy, rg.r, 0, Math.PI * 2)
    snowCtx.stroke()
  })
  snowCtx.restore()
}

function setupSnow() {
  resizeCanvas()
  cancelAnimationFrame(animationId)
  animationId = requestAnimationFrame(animateSnow)
}

onMounted(() => {
  // 延迟一帧保证布局尺寸正确
  requestAnimationFrame(setupSnow)
  window.addEventListener('resize', resizeCanvas)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', resizeCanvas)
  cancelAnimationFrame(animationId)
})

async function login() {
  if (!form.username || !form.password) {
    return ElMessage?.warning?.('请填写用户名和密码') || alert('请填写用户名和密码')
  }
  // 点击即触发一次冲击波视觉效果（不依赖后端成功）
  triggerShockwave()
  loading.value = true
  try {
    const msg = await api.postForm('/user/login', { username: form.username, password: form.password })
    if (!String(msg || '').includes('成功')) {
      throw new Error(String(msg || '登录失败'))
    }
    const role = await api.get('/user/get/role', { username: form.username })
    sessionStorage.setItem('role', String(role))
    const uid = await api.get('/user/getUserId', { username: form.username })
    localStorage.setItem('userId', String(uid || ''))
    localStorage.setItem('username', form.username)
    ElMessage.success('登录成功')
    const roleStr = String(role).toLowerCase()
    if (roleStr === 'admin') {
      const preferred = localStorage.getItem('preferredPanel')
      if (preferred === 'admin') return router.push('/dashboard')
      if (preferred === 'user') return router.push('/u/home')
      return router.push('/entry')
    }
    return router.push('/u/home')
  } catch (e) {
    ElMessage.error(String(e))
  } finally {
    loading.value = false
  }
}
</script>

<style scoped>
.login-wrap {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px clamp(16px, 4vw, 48px);
  background: linear-gradient(
      180deg,
      #1a1f2b 0%, /* 更深夜空 */
      #2b2b40 14%,
      #3d1d2b 32%, /* 过渡 */
      #5e1d25 48%, /* 主红层 */
      #7b221f 62%,
      #992d1d 74%, /* 低空强烈余晖 */
      #b2371c 82%,
      #c94521 88%,
      #d85226 92%,
      #e2602a 96%, /* 接近地平线亮橙红 */
      #f06d30 100%
    );
  position: relative;
  overflow: hidden;
  color: #fff;
}

/* 山景 SVG */
.mountains {
  position: absolute;
  inset: 0 0 0 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
  mix-blend-mode: lighten;
  opacity: .9;
}

/* 雪花画布 */
.snow-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 5;
  pointer-events: none;
}

.login-panel {
  width: 100%;
  max-width: 960px;
  min-height: 520px;
  display: flex;
  gap: 0;
  background: #fff;
  border-radius: 20px;
  box-shadow: 0 10px 38px -6px rgba(40,56,120,0.15), 0 6px 18px -4px rgba(60,72,120,0.10);
  overflow: hidden;
  position: relative;
  z-index: 10;
}

.brand-side {
  flex: 1.1;
  background: linear-gradient(145deg,#5b7bff,#6f89ff,#8aa3ff);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 46px 32px;
  position: relative;
}
.brand-side::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 30% 30%, rgba(255,255,255,.25), rgba(255,255,255,0) 60%);
  pointer-events: none;
}
.brand-inner { max-width: 320px; }
.site-title { margin: 0 0 12px; font-size: 34px; letter-spacing: .5px; }
.site-sub { margin: 0 0 24px; line-height: 1.4; font-size: 14px; opacity: .9; }
.highlights { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px; }
.highlights li { position: relative; padding-left: 18px; font-size: 13px; }
.highlights li::before { content: ''; position: absolute; left: 0; top: 6px; width: 8px; height: 8px; background: #fff; border-radius: 2px; box-shadow: 0 0 0 3px rgba(255,255,255,.35); }

.box {
  flex: 1;
  border: none;
  border-radius: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 46px 54px 40px;
  background: #fff;
}
.card-header h2 { margin: 0 0 4px; font-size: 26px; font-weight: 600; color: #2f3651; }
.subtitle { margin: 0 0 28px; font-size: 13px; color: #6c748b; }
::v-deep(.el-form-item__label) { font-weight: 500; letter-spacing: .5px; color: #39405a; }
::v-deep(.el-input__wrapper) { transition: box-shadow .25s ease, background-color .25s ease; }
::v-deep(.el-input__wrapper:hover) { box-shadow: 0 0 0 1px #aebdff inset; }
::v-deep(.el-input.is-focus .el-input__wrapper),
::v-deep(.el-input__wrapper.is-focus) { box-shadow: 0 0 0 1px #5b7bff inset, 0 0 0 3px rgba(91,123,255,.18); }

.login-btn { width: 100%; height: 44px; font-size: 15px; letter-spacing: 1px; }
.assist-row { margin-top: 8px; }
.tips { margin-top: 4px; color: #6d7894; font-size: 12px; display: flex; align-items: center; gap: 4px; background: #f1f4fb; padding: 8px 10px; border-radius: 8px; line-height: 1.4; }

/* 响应式：窄屏隐藏品牌侧，内容居中 */
@media (max-width: 860px) {
  .login-panel { flex-direction: column; max-width: 420px; min-height: unset; }
  .brand-side { display: none; }
  .box { border-radius: 20px; padding: 40px 38px 34px; }
  .card-header h2 { text-align: center; }
  .subtitle { text-align: center; }
}
@media (max-width: 420px) {
  .box { padding: 36px 26px 30px; }
  .site-title { font-size: 30px; }
}
</style>
